﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using TestXNA3.Actors;
using TestXNA3.Render;

namespace TestXNA3.Helpers
{
    static class TSM
    {
        public static Vector2 Intersect(Vector2 g0, Vector2 g1, Vector2 h0, Vector2 h1)
        {
            Vector2 output = Vector2.Zero;
            output.X = output.Y = 1.0f / MathHelper2D.Determinant(g0.X - g1.X, g0.Y - g1.Y, h0.X - h1.X, h0.Y - h1.Y);

            float a = MathHelper2D.Determinant(g0.X, g0.Y, g1.X, g1.Y);
            float b = MathHelper2D.Determinant(h0.X, h0.Y, h1.X, h1.Y);

            output.X *= MathHelper2D.Determinant(a, g0.X - g1.X, b, h0.X - h1.X);
            output.Y *= MathHelper2D.Determinant(a, g0.Y - g1.Y, b, h0.Y - h1.Y);

            return output;
        }

        public static void MapTrapezoidToSquare(out Matrix TR, Vector2 t0, Vector2 t1, Vector2 t2, Vector2 t3)
        {
            Vector2 i;
            float a, b, c, d;

            //M1 = R * T1
            a = 0.5f * (t2.X - t3.X);
            b = 0.5f * (t2.Y - t3.Y);

            TR = Matrix.Identity;
            TR.M11 = a; TR.M12 = b; TR.M14 = a * a + b * b;
            TR.M21 = b; TR.M22 = -a; TR.M24 = a * b - b * a;
            //return;


            //M2 = T2 * M1 = T2 * R * T1
            i = Intersect(t0, t3, t1, t2);

            TR.M14 = -Vector2.Dot(new Vector2(TR.M11, TR.M12), i);
            TR.M24 = -Vector2.Dot(new Vector2(TR.M21, TR.M22), i);

            //M1 = H * M2 = H * T2 * R * T1
            a = Vector2.Dot(new Vector2(TR.M11, TR.M12), t2) + TR.M14;
            b = Vector2.Dot(new Vector2(TR.M21, TR.M22), t2) + TR.M24;
            c = Vector2.Dot(new Vector2(TR.M11, TR.M12), t3) + TR.M14;
            d = Vector2.Dot(new Vector2(TR.M21, TR.M22), t3) + TR.M24;

            a = -(a + c) / (b + d);

            TR.M11 += TR.M21 * a;
            TR.M12 += TR.M22 * a;
            TR.M14 += TR.M24 * a;

            //M2 = S1 * M1 = S1 * H * T2 * R * T1
            a = 1.0f / (Vector2.Dot(new Vector2(TR.M11, TR.M12), t2) + TR.M14);
            b = 1.0f / (Vector2.Dot(new Vector2(TR.M21, TR.M22), t2) + TR.M24);

            TR.M11 *= a; TR.M12 *= a; TR.M14 *= a;
            TR.M21 *= b; TR.M22 *= b; TR.M24 *= b;

            //M1 = N * M2 = N * S1 * H * T2 * R * T1
            TR.M41 = TR.M21; TR.M42 = TR.M22; TR.M44 = TR.M24;
            TR.M24 += 1.0f;

            //M2 = T3 * M1 = T3 * N * S1 * H * T2 * R * T1
            a = Vector2.Dot(new Vector2(TR.M21, TR.M22), t0) + TR.M24;
            b = Vector2.Dot(new Vector2(TR.M41, TR.M42), t0) + TR.M44;
            c = Vector2.Dot(new Vector2(TR.M21, TR.M22), t2) + TR.M24;
            d = Vector2.Dot(new Vector2(TR.M41, TR.M42), t2) + TR.M44;

            a = -0.5f * (a / b + c / d);

            TR.M21 += TR.M41 * a;
            TR.M22 += TR.M42 * a;
            TR.M24 += TR.M44 * a;

            //M1 = S2 * M2 = S2 * T3 * N * S1 * H * T2 * R * T1
            a = Vector2.Dot(new Vector2(TR.M21, TR.M22), t0) + TR.M24;
            b = Vector2.Dot(new Vector2(TR.M41, TR.M42), t0) + TR.M44;

            c = -b / a;

            TR.M21 *= c; TR.M22 *= c; TR.M24 *= c;

            TR = Matrix.Transpose(TR) * Matrix.CreateRotationZ(MathHelper.ToRadians(180.0f));
        }

        public static void SetTrapezoidResolution(out Matrix N, float percentage, float d1, Ray2D bottom, Ray2D top, Ray2D lineofsight, Vector2[] eyeTrans)
        {
            Vector2 s = bottom.Position;

            float l = Math.Abs(top.SignedDistanceFromPoint(s));
            float d2 = -((2.0f * percentage) / 100.0f - 1.0f);
            float n = (l * d1 + l * d1 * d2) / (l - 2.0f * d1 - l * d2);

            Vector2 u, v;
            MathHelper2D.RayIntersect(out u, top, lineofsight);
            MathHelper2D.RayIntersect(out v, bottom, lineofsight);
            Vector2 w = v - u;

            float t = -n / w.Length();
            Vector2 i = u + w * t;

            float min1, min2, f;
            int i0 = 0, i1 = 0;
            min1 = min2 = System.Single.MaxValue;
            w = i - u;

            w.Normalize();

            for (int j = 4; j < 8; j++)
            {
                Vector2 x = eyeTrans[j] - i;
                x.Normalize();

                f = (float)(Math.Acos(Vector2.Dot(x, w)));
                if (MathHelper2D.CounterClockwise(u, v, eyeTrans[j]) < 0.0f)
                {
                    if (f < min1)
                    {
                        min1 = f;
                        i0 = j;
                    }
                }
                else
                {
                    if (f < min2)
                    {
                        min2 = f;
                        i1 = j;
                    }
                }
            }

            Vector2 left = eyeTrans[i0], right = eyeTrans[i1];

            Ray2D h = new Ray2D(i, right - i);
            Ray2D g = new Ray2D(i, left - i);

            Vector2 t0, t1, t2, t3;
            MathHelper2D.RayIntersect(out t0, bottom, h);
            MathHelper2D.RayIntersect(out t1, bottom, g);
            MathHelper2D.RayIntersect(out t2, top, g);
            MathHelper2D.RayIntersect(out t3, top, h);

            //MapTrapezoidToSquare(out N, t0, t1, t2, t3);
            MapTrapezoidToSquare(out N, t1, t0, t3, t2);
            //MapTrapezoidToSquare(out N, t0, t3, t2, t1);
            //MapTrapezoidToSquare(out N, t3, t2, t1, t0);
            //MapTrapezoidToSquare(out N, t2, t1, t0, t3);
        }

        // Calculate Frustum Vertices in World Space with specified nearPlane and farPlane
        private static Vector3[] GetFrustumWorld(float znear, float zfar, ref SceneRenderer.ViewerDesc viewer)
        {
            float aspectRatio = viewer.AspectRatio;
            float tanThetaOver2 = (float)Math.Tan(MathHelper.ToRadians(viewer.Fov / 2.0f));

            float ynear = tanThetaOver2 * znear;
            float xnear = ynear * aspectRatio;

            float yfar = tanThetaOver2 * zfar;
            float xfar = yfar * aspectRatio;

            Vector3[] frustumWorld = new Vector3[8];

            frustumWorld[0] = viewer.Location - xnear * viewer.Right - ynear * viewer.Up - znear * viewer.Forward;
            frustumWorld[1] = viewer.Location + xnear * viewer.Right - ynear * viewer.Up - znear * viewer.Forward;
            frustumWorld[2] = viewer.Location + xnear * viewer.Right + ynear * viewer.Up - znear * viewer.Forward;
            frustumWorld[3] = viewer.Location - xnear * viewer.Right + ynear * viewer.Up - znear * viewer.Forward;

            frustumWorld[4] = viewer.Location - xfar * viewer.Right - yfar * viewer.Up - zfar * viewer.Forward;
            frustumWorld[5] = viewer.Location + xfar * viewer.Right - yfar * viewer.Up - zfar * viewer.Forward;
            frustumWorld[6] = viewer.Location + xfar * viewer.Right + yfar * viewer.Up - zfar * viewer.Forward;
            frustumWorld[7] = viewer.Location - xfar * viewer.Right + yfar * viewer.Up - zfar * viewer.Forward;

            return frustumWorld;
        }

        public static Matrix ComputeTsmMatrix(ref SceneRenderer.ViewerDesc viewer, Matrix lightView, Matrix lightProjection
                                                , float tsmDistance,
                                              float percentage, int shadowMapHeight)
        {
            // Let's get the frustum of the camera and tsm's camera
            Vector3[] camFrustumWorld = GetFrustumWorld(viewer.NearClip, viewer.FarClip, ref viewer);
            Vector3[] tsmFrustumWorld = GetFrustumWorld(viewer.NearClip, tsmDistance, ref viewer);

            // Get Transformation Matrix from world coord to light's clip space
            Matrix lightViewProjection = lightView * lightProjection;
            Vector2[] camFrustumDevice = new Vector2[8];
            Vector2[] tsmFrustumDevice = new Vector2[8];

            for (int i = 0; i < 8; ++i)
            {
                // Transform cam's frustum to light's clip space and do perspective division
                Vector3 v = Vector3.Transform(camFrustumWorld[i], lightViewProjection);
                camFrustumDevice[i].X = v.X / v.Z;
                camFrustumDevice[i].Y = v.Y / v.Z;

                // Transform tsm's frustum to light's clip space and do perspective division
                v = Vector3.Transform(tsmFrustumWorld[i], lightViewProjection);
                tsmFrustumDevice[i].X = v.X / v.Z;
                tsmFrustumDevice[i].Y = v.Y / v.Z;
            }

            // Calculate the center of the near plane
            Vector3 nearCenterClip = camFrustumWorld[0] + (camFrustumWorld[2] - camFrustumWorld[0]) * 0.5f;
            Vector3 farCenterClip = camFrustumWorld[4] + (camFrustumWorld[6] - camFrustumWorld[4]) * 0.5f;

            // Transform near center and far center to light's clip space            
            nearCenterClip = Vector3.Transform(nearCenterClip, lightViewProjection);
            farCenterClip = Vector3.Transform(farCenterClip, lightViewProjection);

            // Do perspective division to get near center and far center in light's normalized device space
            Vector2 nearCenterDevice = new Vector2(nearCenterClip.X / nearCenterClip.Z, nearCenterClip.Y / nearCenterClip.Z);
            Vector2 farCenterDevice = new Vector2(farCenterClip.X / farCenterClip.Z, farCenterClip.Y / farCenterClip.Z);

            // Create line of sight from camera frustum
            Ray2D camLos = new Ray2D(nearCenterDevice, farCenterDevice - nearCenterDevice);

            // Create camera convex hull and tsm convex hull            
            ConvexHull2D camConvexHull = new ConvexHull2D(camFrustumDevice);
            ConvexHull2D tsmConvexHull = new ConvexHull2D(tsmFrustumDevice);

            Ray2D orthoCamLos = camLos.Ortho();
            Vector2 maxPointBottom = orthoCamLos.MaxPointToLine(camConvexHull.vertices);

            Ray2D bottomLine = orthoCamLos.Parallel(maxPointBottom);
            Vector2 maxPointTop = bottomLine.MaxPointToLine(camConvexHull.vertices);

            Ray2D topLine = bottomLine.Parallel(maxPointTop);
            Vector2 maxPointTsm = topLine.MaxPointToLine(tsmConvexHull.vertices);

            float d1 = Math.Abs(topLine.SignedDistanceFromPoint(maxPointTsm));

            Matrix N = Matrix.Identity;
            float maxArea = 0.0f;
            float inc = 100.0f / shadowMapHeight;
            for (float g = percentage; g >= 0.0f; g -= inc)
            {
                SetTrapezoidResolution(out N, g, d1, bottomLine, topLine, camLos, camFrustumDevice);

                Vector2[] ch = new Vector2[tsmConvexHull.vertices.Length];
                for (int i = 0; i < ch.Length; ++i)
                {
                    Vector4 v = Vector4.Transform(
                                    new Vector4(tsmConvexHull.vertices[i].X, tsmConvexHull.vertices[i].Y, 0.0f, 1.0f),
                                    N);
                    ch[i] = new Vector2(v.X / v.W, v.Y / v.W);
                }

                float f = ConvexHull2D.FindArea(ch);
                if (f < maxArea)
                {
                    g += inc;
                    break;
                }
                else
                {
                    maxArea = f;
                }
            }

            return N;
        }

        public static void SetTrapezoidResolutionDebug(ref Matrix N, float percentage, float d1, Ray2D bottom, Ray2D top, Ray2D lineofsight, Vector2[] eyeTrans,
                            out Vector2 o0, out Vector2 o1, out Vector2 o2, out Vector2 o3)
        {
            Vector2 s = bottom.Position;

            float l = Math.Abs(top.SignedDistanceFromPoint(s));
            float d2 = -((2.0f * percentage) / 100.0f - 1.0f);
            float n = (l * d1 + l * d1 * d2) / (l - 2.0f * d1 - l * d2);

            Vector2 u, v;
            MathHelper2D.RayIntersect(out u, top, lineofsight);
            MathHelper2D.RayIntersect(out v, bottom, lineofsight);
            Vector2 w = v - u;

            float t = -n / w.Length();
            Vector2 i = u + w * t;

            float min1, min2, f;
            int i0 = 0, i1 = 0;
            min1 = min2 = System.Single.MaxValue;
            w = i - u;

            w.Normalize();

            for (int j = 4; j < 8; j++)
            {
                Vector2 x = eyeTrans[j] - i;
                x.Normalize();

                f = (float)(Math.Acos(Vector2.Dot(x, w)));
                if (MathHelper2D.CounterClockwise(u, v, eyeTrans[j]) < 0.0f)
                {
                    if (f < min1)
                    {
                        min1 = f;
                        i0 = j;
                    }
                }
                else
                {
                    if (f < min2)
                    {
                        min2 = f;
                        i1 = j;
                    }
                }
            }

            Vector2 left = eyeTrans[i0], right = eyeTrans[i1];

            Ray2D h = new Ray2D(i, right - i);
            Ray2D g = new Ray2D(i, left - i);

            Vector2 t0, t1, t2, t3;
            MathHelper2D.RayIntersect(out t0, bottom, h);
            MathHelper2D.RayIntersect(out t1, bottom, g);
            MathHelper2D.RayIntersect(out t2, top, g);
            MathHelper2D.RayIntersect(out t3, top, h);

            o0 = t0;
            o1 = t1;
            o2 = t2;
            o3 = t3;

            MapTrapezoidToSquare(out N, t0, t1, t2, t3);

        }

    }
}
